本文同步更新於blog
情境:平台有三種身份,分別是訪客 (guest)、會員 (member)及付費會員(premium)
<?php
namespace App\StatePattern\Youtube;
use Exception;
class Program
{
    /**
     * @var string
     */
    protected $license;
    public function __construct()
    {
        $this->setLicense('guest');
    }
    public function register()
    {
        if ($this->license == 'premium') {
            return;
        }
        $this->license = 'member';
    }
    public function getLicense()
    {
        return $this->license;
    }
    /**
     * @param string $license
     */
    public function setLicense($license)
    {
        $this->license = $license;
    }
    public function subscribe()
    {
        if ($this->license == 'premium') {
            return;
        }
        if ($this->license == 'member') {
            $this->license = 'premium';
            return;
        }
        throw new Exception('You need to be a member before subscribing.');
    }
    public function cancelSubscription()
    {
        if ($this->license == 'premium') {
            $this->license = 'member';
            return;
        }
        throw new Exception('Sorry, you have not subscribed.');
    }
    public function deleteAccount()
    {
        if ($this->license == 'member' || $this->license == 'premium') {
            $this->license = 'guest';
            return;
        }
        throw new Exception('You need to be a member before deleting account.');
    }
}
隨著功能越來越多,每次新增功能時,
我們都會有許多複雜的條件式(判斷當前用戶狀態)。
讓我們用狀態模式改善它。
需求一:切分出不同的用戶狀態
<?php
namespace App\StatePattern\Youtube\State;
abstract class UserState
{
    const LICENCE = 'undefined user';
    public function getLicense()
    {
        return $this::LICENCE;
    }
    abstract function register();
    abstract function subscribe();
    abstract function cancelSubscription();
    abstract function deleteAccount();
}
<?php
namespace App\StatePattern\Youtube\State;
use App\StatePattern\Youtube\Program;
use Exception;
use App\StatePattern\Youtube\State\UserState;
class GuestState extends UserState
{
    /**
     * @var Program
     */
    protected $program;
    /**
     * @var string
     */
    const LICENCE = 'guest';
    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }
    public function register()
    {
        $this->program->setMemberState();
    }
    public function subscribe()
    {
        throw new Exception('You need to be a member before subscribing.');
    }
    public function cancelSubscription()
    {
        throw new Exception('Sorry, you have not subscribed.');
    }
    public function deleteAccount()
    {
        throw new Exception('You need to be a member before deleting account.');
    }
}
<?php
namespace App\StatePattern\Youtube\State;
use App\StatePattern\Youtube\Program;
use Exception;
use App\StatePattern\Youtube\State\UserState;
class MemberState extends UserState
{
    /**
     * @var Program
     */
    protected $program;
    /**
     * @var string
     */
    const LICENCE = 'member';
    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }
    public function register()
    {
        return;
    }
    public function subscribe()
    {
        $this->program->setPremiumState();
    }
    public function cancelSubscription()
    {
        throw new Exception('Sorry, you have not subscribed.');
    }
    public function deleteAccount()
    {
        $this->program->setGuestState();
    }
}
<?php
namespace App\StatePattern\Youtube\State;
use App\StatePattern\Youtube\Program;
use App\StatePattern\Youtube\State\UserState;
class PremiumState extends UserState
{
    /**
     * @var Program
     */
    protected $program;
    /**
     * @var string
     */
    const LICENCE = 'premium';
    /**
     * @param Program $program
     */
    public function __construct(Program $program)
    {
        $this->program = $program;
    }
    public function register()
    {
        return;
    }
    public function subscribe()
    {
        return;
    }
    public function cancelSubscription()
    {
        $this->program->setMemberState();
    }
    public function deleteAccount()
    {
        $this->program->setGuestState();
    }
}
<?php
namespace App\StatePattern\Youtube;
use App\StatePattern\Youtube\State\MemberState;
use App\StatePattern\Youtube\State\GuestState;
use App\StatePattern\Youtube\State\PremiumState;
use App\StatePattern\Youtube\State\UserState;
class Program
{
    /**
     * @var string
     */
    protected $license;
    /**
     * @var MemberState
     */
    protected $memberState;
    /**
     * @var GuestState
     */
    protected $guestState;
    /**
     * @var PremiumState
     */
    protected $premiumState;
    /**
     * @var UserState
     */
    protected $state;
    public function __construct()
    {
        $this->memberState = new MemberState($this);
        $this->guestState = new GuestState($this);
        $this->premiumState = new PremiumState($this);
        $this->setGuestState();
    }
    public function register()
    {
        $this->state->register();
    }
    public function getLicense()
    {
        return $this->state->getLicense();
    }
    public function subscribe()
    {
        $this->state->subscribe();
    }
    public function cancelSubscription()
    {
        $this->state->cancelSubscription();
    }
    public function setGuestState()
    {
        $this->state = $this->guestState;
    }
    public function setMemberState()
    {
        $this->state = $this->memberState;
    }
    public function setPremiumState()
    {
        $this->state = $this->premiumState;
    }
    public function getState()
    {
        return $this->state;
    }
    public function deleteAccount()
    {
        $this->state->deleteAccount();
    }
}
[單一職責原則]
我們將情境類別與狀態類別視作兩種不同的職責。
透過委派來實現不同狀態下的行為。
[開放封閉原則]
修改既有狀態類別的行為,不會影響到全部的既有狀態類別。
(新增狀態類別時,可能會影響到)
[介面隔離原則]
情境類別依賴於抽象的用戶狀態 (User State)。
不同的狀態類別實作抽象的用戶狀態 (User State)。
最後附上類別圖:
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)
ʕ •ᴥ•ʔ:透過測試,重構這個範例時有遇到一些小困難。
大家也可以挑戰看看。